# -*- coding: utf-8 -*-
import os
import uuid
import aiofiles
import traceback
from typing import List

from loguru import logger
from pydantic import BaseModel
from fastapi import Request, UploadFile, File, Form, BackgroundTasks
from starlette.requests import ClientDisconnect

from schemas.response_entity import JsonModel
from configs import UPLOAD_FILE_DIR, ES_KB_INDEX_NAME, ES_KB_EMERGENCY_NAME
from utils.file_utils import convert_docx_to_pdf, normalize_filename_extension
from utils.hash_utils import cal_md5
from service.es_service import ELASTICSEARCH, ELASTICSEARCH_EMERGENCY
from db.repository.qa_file import db_get_qa_file_by_id, db_update_qa_file_scope_gb, db_update_qa_file_attributes, \
    db_get_qa_file_by_name, db_add_qa_file, db_file_add_asso, db_get_qa_files, db_get_qa_files_by_user_groups, \
    db_update_qa_file_delete_state, db_completely_delete_qa_file
from .utils import parse_kb_pdf_background


class UpdateFileEntity(BaseModel):
    file_id: uuid.UUID
    file_name: str = None
    applicable_object: List[int] = None
    knowledge_type: List[int] = None
    applicable_area: List[int] = None
    element_classification: List[int] = None
    applicable_scope: str = None
    gb_num: str = None
    token: str


# 问答库文件列表
# 功能：根据用户角色返回可见的问答文件列表（超级管理员全部，其余按组过滤）
async def show_file_list(request: Request, page: int = 1, page_size: int = 50, name: str = ""):
    user = request.state.user
    try:
        offset = (page - 1) * page_size
        if user.check_superman_role():
            file_list, total = db_get_qa_files(name_like=name or None, offset=offset, limit=page_size)
        else:
            # 非超级管理员只能查看该用户所在组的可查看文件
            from db.repository.user import db_get_user_group_ids
            user_group_ids = db_get_user_group_ids(user)
            file_list, total = db_get_qa_files_by_user_groups(user_group_ids, name_like=name or None, offset=offset, limit=page_size)
        
        return JsonModel(
            code=200, 
            data={
                "items": [i.to_json() for i in file_list],
                "total": total,
                "page": page,
                "page_size": page_size
            }, 
            msg="Success"
        ).to_response()
    except Exception as e:
        logger.error(f"查询文件列表失败: {str(e)}")
        return JsonModel(code=500, data=None, msg="查询失败").to_response()


# 上传问答库文件
# 功能：管理员上传PDF/Word，按内容去重保存并触发后台解析
async def upload_file(
        request: Request,
        background: BackgroundTasks,
        token=Form(...),
        file: UploadFile = File(...),  # 必需文件参数放前面
        file_type: str = Form(...),  # 必需表单参数
        applicable_object: List[int] = Form(None),
        knowledge_type: List[int] = Form(None),
        applicable_area: List[int] = Form(None),
        element_classification: List[int] = Form(None),
):
    """
    上传文件
    1. 验证文件类型
    2. 计算文件的 MD5 值与文件大小
    3. 文件不存在：保存到本地
    4. 文件存在：关联数据库, is_delete=True则删除本地解析文件重新解析
    5. 解析文件
    """
    user = request.state.user
    if not user.check_admin_role():
        return JsonModel(code=401, data=None, msg=f"用户无权限").to_response()

    # 验证文件类型
    allowed_extensions = {'pdf', 'doc', 'docx', 'DOC', 'DOCX', 'PDF'}
    normalized_filename = normalize_filename_extension(file.filename)
    if '.' in normalized_filename:
        file_suffix = normalized_filename.rsplit('.', 1)[-1]
    else:
        file_suffix = ""

    if file_suffix not in allowed_extensions:
        return JsonModel(code=400, data=None,
                         msg=f"不支持的文件类型: {file_suffix}，仅支持 {', '.join(allowed_extensions)}").to_response()

    # 计算文件的 MD5 值与文件大小
    try:
        file_content = await file.read()
        if not file_content:
            return JsonModel(code=400, data=None, msg="文件内容为空").to_response()
        file_md5 = cal_md5(file_content)
        file_hash_name = f"{file_md5}.{file_suffix}"
        file_size = len(file_content)
    except Exception as e:
        logger.error(f"读取文件失败: {str(e)}")
        return JsonModel(code=500, data=None, msg="文件读取失败，请检查文件是否完整").to_response()

    file_obj = db_get_qa_file_by_name(file_hash_name)
    if file_obj:
        return JsonModel(code=403, data=None, msg="文件已存在，关联数据库即可").to_response()

    # 文件不存在：保存到本地
    dir_path = os.path.join(UPLOAD_FILE_DIR, file_md5)
    if not os.path.exists(dir_path):
        os.makedirs(dir_path)
    file_hash_path = os.path.join(dir_path, file_hash_name)

    try:
        async with aiofiles.open(file_hash_path, "wb") as f:
            await f.write(file_content)
    except IOError as e:
        logger.error(f"文件保存失败: {str(e)}")
        return JsonModel(code=500, data=None, msg="文件保存失败").to_response()

    file_obj, is_delete = db_add_qa_file(user.id, file_hash_name, normalized_filename, file_hash_path, file_suffix, file_size)

    # doc/docx -> pdf
    if file_suffix.lower() in ['doc', 'docx']:
        pdf_path_in_target = os.path.join(dir_path, f"{file_md5}.pdf")
        if not os.path.exists(pdf_path_in_target):
            r = convert_docx_to_pdf(file_hash_path, pdf_path_in_target)
            if r:
                file_pdf_path = pdf_path_in_target
                logger.debug(f"converted word to pdf: {pdf_path_in_target}")
            else:
                logger.error(f"word转pdf失败: {file_hash_path}")
                return JsonModel(code=500, data=None, msg="word转pdf失败").to_response()
        else:
            # PDF已存在，直接使用
            file_pdf_path = pdf_path_in_target
            logger.debug(f"PDF already exists, using: {pdf_path_in_target}")

    else:
        file_pdf_path = file_hash_path

    try:
        # 修正后的属性处理逻辑
        attributes = [
            ("applicable_object", applicable_object),
            ("knowledge_type", knowledge_type),
            ("applicable_area", applicable_area),
            ("element_classification", element_classification)
        ]

        for attr_type, attr_value in attributes:
            if attr_value:  # 如果列表不为空
                # 假设db_file_add_asso可以处理列表值
                # 如果需要单独处理每个元素，可以这样：
                db_file_add_asso(file_obj.id, attr_type, attr_value)

        background.add_task(parse_kb_pdf_background, file_obj, file_type, file_pdf_path, is_delete)
        return JsonModel(code=200, data={"id": str(file_obj.id)}, msg=f"上传文件:{normalized_filename}成功").to_response()
    except ClientDisconnect:
        logger.warning("客户端断开连接，文件上传中断")
        try:
            if 'file_obj' in locals():
                db_update_qa_file_delete_state(file_obj.id, delete=True)
        except Exception as cleanup_error:
            logger.error(f"清理文件状态失败: {cleanup_error}")
        return JsonModel(code=499, data=None, msg="客户端断开连接，文件上传中断").to_response()
    except Exception as e:
        logger.error(f"处理上传文件出错: {e}\n{traceback.format_exc()}")
        try:
            if 'file_obj' in locals():
                db_update_qa_file_delete_state(file_obj.id, delete=True)
        except Exception as cleanup_error:
            logger.error(f"清理文件状态失败: {cleanup_error}")
        return JsonModel(code=500, data=None, msg="文件上传处理失败").to_response()


# 完全删除问答库文件
# 功能：删除ES向量、数据库记录与本地文件及关联，仅管理员可执行
async def delete_file(request: Request, file_id: uuid.UUID):
    """完全删除文件：包括数据库记录、本地文件和所有关联"""
    user = request.state.user
    # 权限校验：仅管理员可删
    if not user.check_admin_role():
        return JsonModel(code=400, data=None, msg="用户无权限").to_response()

    try:
        # 1. 删除 ES 中该文件的向量文档
        query = {
            "query": {
                "term": {
                    "metadata.file_id.keyword": str(file_id)
                }
            }
        }

        # 分别删除两个索引中的文档，若其中一个不存在则忽略
        try:
            res_main = ELASTICSEARCH.delete_doc_by_query(
                idxnm=ES_KB_INDEX_NAME,
                query=query,
            )
            logger.info(f"已删除主索引文档: {file_id}")
        except Exception as e:
            logger.warning(f"删除主索引文档失败: {e}")

        # 2. 完全删除文件及其所有关联
        success, message = db_completely_delete_qa_file(file_id)

        if success:
            return JsonModel(code=200, data=None, msg=f"文件完全删除成功: {message}").to_response()
        else:
            return JsonModel(code=400, data=None, msg=f"文件删除失败: {message}").to_response()

    except Exception as e:
        logger.error(f"删除文件时发生错误: {str(e)}")
        return JsonModel(code=500, data=None, msg=f"删除文件失败: {str(e)}").to_response()


# 更新文件属性
# 更新问答库文件属性
# 功能：更新文件基础属性与属性关联，并同步更新ES中的文件名
async def update_file_info(request: Request, payload: UpdateFileEntity):
    """
    更改文件属性
    包括但不限于：适用范围、要素分类\适用对象\知识类型\适用区域
    数据库和向量库同步更改
    """
    user = request.state.user

    file_id = payload.file_id
    file_name = payload.file_name
    applicable_object = payload.applicable_object
    applicable_area = payload.applicable_area
    knowledge_type = payload.knowledge_type
    element_classification = payload.element_classification
    applicable_scope = payload.applicable_scope
    gb_num = payload.gb_num

    try:
        file = db_get_qa_file_by_id(file_id)

        if not file:
            return JsonModel(code=400, data=None, msg=f"文件未找到存在,请重试").to_response()

        file_old_name = file.name

        updated_file = db_update_qa_file_scope_gb(file_id, file_name, gb_num, applicable_scope)
        db_update_qa_file_attributes(file_id, "applicable_object", applicable_object)
        db_update_qa_file_attributes(file_id, "knowledge_type", knowledge_type)
        db_update_qa_file_attributes(file_id, "applicable_area", applicable_area)
        db_update_qa_file_attributes(file_id, "element_classification", element_classification)

        if file_name and (file_name != file_old_name):
            update_query = {
                "query": {
                    "term": {
                        "metadata.file_id.keyword": str(file_id)  # 确保这里的字段名和类型正确
                    }
                },
                "script": {
                    "source": """ctx._source.metadata.file_name = params.new_file_name;""",
                    "lang": "painless",
                    "params": {
                        "new_file_name": file_name
                    }
                }
            }
            # 主索引更新，忽略不存在错误
            try:
                res = ELASTICSEARCH.update_by_query(
                    index=ES_KB_INDEX_NAME,
                    update_query=update_query,
                )
            except Exception as e:
                logger.warning(f"ES_KB 索引更新失败: {e}")

            # 应急索引更新，忽略不存在错误
            try:
                res2 = ELASTICSEARCH_EMERGENCY.update_by_query(
                    index=ES_KB_EMERGENCY_NAME,
                    update_query=update_query,
                )
            except Exception as e:
                logger.warning(f"ES_KB_EMERGENCY 索引更新失败: {e}")

        return JsonModel(code=200, data=None, msg="success").to_response()

    except Exception as e:
        logger.error(f"更新文件信息失败: {str(traceback.format_exc())}")
        return JsonModel(code=500, data=None, msg="更新文件信息失败").to_response()

